﻿package joystick
{
	import flash.display.Bitmap;
	import flash.display.Sprite;
	import flash.errors.EOFError;
	import flash.errors.IOError;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.IOErrorEvent;
	import flash.events.MouseEvent;
	import flash.events.ProgressEvent;
	import flash.events.SecurityErrorEvent;
	import flash.events.TimerEvent;
	import flash.net.navigateToURL;
	import flash.net.Socket;
	import flash.net.URLRequest;
	import flash.system.Security;
	import flash.utils.ByteArray;
	import flash.utils.Endian;
	import flash.utils.Timer;
	import joystick.events.JoyClientEvent;
	
	// ------------------------------------------------------------------------
	
	/**
	 * 接続完了時に呼び出される
	 * @eventType joystick.events.ClientEvent.CONNECTED
	 */
	[Event(name = "clientConnected", type = "joystick.events.ClientEvent")] 
	
	/**
	 * ソケット切断時に呼び出される
	 * @eventType joystick.events.ClientEvent.CLOSED
	 */
	[Event(name = "clientClosed", type = "joystick.events.ClientEvent")] 
	
	/**
	 * IO エラー時に呼び出される
	 * @eventType joystick.events.ClientEvent.IO_ERROR
	 */
	[Event(name = "clientIoError", type = "joystick.events.ClientEvent")] 
	
	/**
	 * セキュリティエラー時に呼び出される
	 * @eventType joystick.events.ClientEvent.SECURITY_ERROR
	 */
	[Event(name = "clientSecurityError", type = "joystick.events.ClientEvent")] 
	
	/**
	 * 正常にパケットが送信できた後に呼び出される
	 * @eventType joystick.events.ClientEvent.PACKET_SENT
	 */
	[Event(name = "clientPacketSent", type = "joystick.events.ClientEvent")] 
	
	/**
	 * 正常にパケットが受信できた後に呼び出される
	 * @eventType joystick.events.ClientEvent.PACKET_RECEIVED
	 */
	[Event(name = "clientPacketReceived", type = "joystick.events.ClientEvent")] 
	
	/**
	 * 正常にデバイスリストが受信できた後に呼び出される
	 * @eventType joystick.events.ClientEvent.DEVICE_LIST
	 */
	[Event(name = "clientDeviceList", type = "joystick.events.ClientEvent")] 
	
	// ------------------------------------------------------------------------
	
	/**
	 * JoyServer クライアント.
	 * <p>
	 * サーバとデータをやりとりするクラス。<br />
	 * <br />
	 * ・パケットフォーマット<br />
	 * [(1):16Bit][(2):16Bit(1)][(3):任意の長さ]...<br />
	 * <br />
	 * (1) : パケットタイプ<br />
	 * (2) : データ長 (バイト単位)<br />
	 * (3) : データ<br />
	 * <br />
	 * パケットタイプを使って、アプリケーション内のパケットの種類を特定する。<br />
	 * データ長はその後に続くデータの長さを表す。<br />
	 * データの項目にはバイナリデータが入る。
	 * </p>
	 * @author Hikipuro
	 */
	public class JoyClient extends EventDispatcher
	{
		/**
		 * ライブラリの名前.
		 */
		public static const name:String = "JoyClient";
		
		/**
		 * ライブラリのバージョン.
		 */
		public static const version:String = "0.9.0";
		
		/**
		 * ライブラリのリリース日.
		 */
		public static const releaseDate:String = "2009/12/17";
		
		/**
		 * ライブラリの配布元 URL.
		 */
		public static const url:String = "http://hikipuro.is-a-geek.org/flash/joyserver/";
		
		/**
		 * JoyClient のロゴ画像.
		 */
		[Embed(source='../../image/JoyClient_logo.png')]
		private static const joyClientLogoImage:Class;
		
		/**
		 * JoyClient のロゴ画像 (マウスオーバー時).
		 */
		[Embed(source='../../image/JoyClient_logo_over.png')]
		private static const joyClientLogoOverImage:Class;
		
		// --------------------------------------------------------------------
		
		/**
		 * デバイスリストリクエスト用パケットのパケットタイプ番号.
		 */
		private const DEVICE_LIST_PACKET_TYPE:uint = 101;
		
		/**
		 * デバッグモード.
		 */
		private var _debugMode:Boolean;
		
		/**
		 * ソケットオブジェクト.
		 */
		private var _socket:Socket;
		
		/**
		 * 現在受信中のパケット.
		 */
		private var _packet:Packet;
		
		/**
		 * パケット組み立てフェーズ.
		 */
		private var _packetPhase:uint;
		
		/**
		 * 次のパケット組み立てフェーズで必要になるバイト数.
		 */
		private var _packetNecessaryBytesLength:uint;
		
		/**
		 * データのエンディアン.
		 * (Socket のメンバ)
		 */
		private var _endian:String;
		
		/**
		 * 接続完了までのタイムアウト時間.
		 * (Socket のメンバ)
		 */
		private var _timeout:uint;
		
		/**
		 * Ping カウントチェッカー.
		 */
		private var _pingChecker:PingChecker;
		
		/**
		 * 1 フレーム前のジョイスティックの情報.
		 */
		private var _prevState:JoystickState;
	
		// --------------------------------------------------------------------
		// Public メソッド
		// --------------------------------------------------------------------
		
		
		/**
		 * JoyClient のロゴを取得する.
		 * <p>
		 * JoyServer 対応の Flash を公開される時は、タイトル画面にロゴを入れて頂けると嬉しいです。
		 * ロゴ自体が押しボタンになっていて、クリックすると JoyServer の配布元サイトにジャンプするようになっています。
		 * </p>
		 * @example
		 * <listing version="3.0">
		 * // 入れ方の例
		 * var logo:Sprite = JoyClient.logo;
		 * logo.x = 10;
		 * logo.y = 10;
		 * addChild(logo);
		 * </listing>
		 */
		public static function get logo():Sprite
		{
			var bitmap1:Bitmap = new joyClientLogoImage;
			var bitmap2:Bitmap = new joyClientLogoOverImage;
			var logo:Sprite = new Sprite();
			var added:Boolean = false;
			logo.addChild(bitmap1);
			logo.buttonMode = true;
			logo.useHandCursor = true;
			
			var mouseDownEvent:Function = function (e:MouseEvent):void 
			{
				var urlRequest:URLRequest = new URLRequest(url);
				navigateToURL(urlRequest, "_blank");
				if (added) {
					added = false;
					logo.removeChild(bitmap2);
				}
			};
			var rollOverEvent:Function = function (e:MouseEvent):void 
			{
				added = true;
				logo.addChild(bitmap2);
			};
			var rollOutEvent:Function = function (e:MouseEvent):void 
			{
				if (added) {
					added = false;
					logo.removeChild(bitmap2);
				}
			};
			
			logo.addEventListener(Event.ADDED_TO_STAGE, function (e:Event):void 
			{
				logo.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownEvent);
				logo.addEventListener(MouseEvent.ROLL_OVER, rollOverEvent);
				logo.addEventListener(MouseEvent.ROLL_OUT, rollOutEvent);
			});
			logo.addEventListener(Event.REMOVED_FROM_STAGE, function (e:Event):void 
			{
				logo.removeEventListener(MouseEvent.MOUSE_DOWN, mouseDownEvent);
				logo.removeEventListener(MouseEvent.ROLL_OVER, rollOverEvent);
				logo.removeEventListener(MouseEvent.ROLL_OUT, rollOutEvent);
			});
			return logo;
		}
		
		/**
		 * コンストラクタ.
		 */
		public function JoyClient() 
		{
			reset();
		}
		
		/**
		 * 状態をリセットする.
		 */
		public function reset():void 
		{
			// ソケットを閉じる
			close();
			
			// 変数の初期化
			_debugMode = false;
			_packet = null;
			_packetPhase = 0;
			_packetNecessaryBytesLength = 0;
			_endian = Endian.LITTLE_ENDIAN;
			_timeout = 20000;
			_prevState = null;
		
			// Ping カウントチェッカーの初期化
			_pingChecker = new PingChecker(8);
			_pingChecker.addEventListener(TimerEvent.TIMER, onTimer);
		}
		
		/**
		 * サーバから切断する.
		 */
		public function close():void 
		{
			if (_socket == null) {
				return;
			}
			_socket.close();
			_socket = null;
		}
		
		/**
		 * サーバと接続されているか確認する.
		 * @return	接続されている場合 true
		 */
		public function isConnected():Boolean
		{
			if (_socket == null)
				return false;
			return _socket.connected;
		}
		
		/**
		 * サーバと接続する.
		 * @param	serverName	接続先サーバ名
		 * @param	port		接続先ポート
		 * @param	policyPort	ポリシーファイルを取得するポート
		 * @param	appName		接続するアプリケーションの名前
		 * @return	接続リクエストを送るところまでいけたら True, エラーが起こって途中で終了した場合は False
		 */
		public function connect(serverName:String = "localhost", port:uint = 17996, policyPort:uint = 843):Boolean 
		{
			// 引数チェック
			if (serverName == "") {
				debugTrace("connect() : サーバ名を指定してください。");
				return false;
			}
			if (port < 1024) {
				debugTrace("connect() : 接続先ポートは 1024 以上を指定する必要があります。");
				return false;
			}
			if (policyPort == 0) {
				debugTrace("connect() : ポリシーファイルを取得するポートを指定してください。");
				return false;
			}
			
			// ポリシーファイルを取得する
			// Flash Player 9 以降？ では、ポリシーファイルを xmlsocket:// で取得しないと
			// ソケット通信が開始できないようになったそうです。
			// http:// で、crossdomain.xml を取得するだけでは
			// ソケット通信が許可できないようになっています。
			// これは、同一ドメイン内のソケットに接続する場合でも必要になります。
			Security.loadPolicyFile("xmlsocket://" + 
									serverName + ":" + 
									policyPort);
			
			// ソケットオブジェクトを作成
			_socket = new Socket();
			
			// イベントハンドラ登録
			_socket.addEventListener(Event.CONNECT, onConnect);
			_socket.addEventListener(Event.CLOSE, onClose);
			_socket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData);
			_socket.addEventListener(IOErrorEvent.IO_ERROR, onIoError);
			_socket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onSecurityError);
		
			// データの並び順を設定
			_socket.endian = _endian;
			
			// タイムアウト時間を設定
			//_socket.timeout = _timeout;
			
			// セキュリティサンドボックスタイプの表示
			debugTrace("connect() : セキュリティサンドボックスタイプ : " + Security.sandboxType);
			
			// サーバに接続
			try {
				debugTrace("connect() : 接続を開始します。");
				//socket.timeout = 5000;
				_socket.connect(serverName, port);
			}
			catch (e:Error)
			{
				debugTrace("connect() : socket.connect() で例外が発生しました。 : " + 
						   e.message + "\n" + e.getStackTrace());
			
				var clientEvent:JoyClientEvent;
				clientEvent = new JoyClientEvent(JoyClientEvent.IO_ERROR);
				clientEvent.text = "JoyClient.connect() : " +
								   "socket.connet でエラーが発生しました。 " + 
								   e.toString();
				dispatchEvent(clientEvent);
				return false;
			}
			
			return true;
		}
		
		/**
		 * ジョイスティックの情報を取得するリクエストを送る.
		 * @param	joystickNumber	ジョイスティック番号
		 */
		public function getState(joystickNumber:uint):void
		{
			if (joystickNumber >= 100)
				return;
			
			var packet:Packet = new Packet();
			packet.type = joystickNumber;
			sendPacket(packet);
		}
		
		/**
		 * デバイスのリストを取得するリクエストを送る.
		 */
		public function getDeviceList():void 
		{
			var packet:Packet = new Packet();
			packet.type = DEVICE_LIST_PACKET_TYPE;
			sendPacket(packet);
		}
		
		/**
		 * パケットを送信する.
		 * @param	packet	送信するパケット
		 * @return	送信できた時 True
		 */
		private function sendPacket(packet:Packet):Boolean
		{
			// null チェック
			if (_socket == null) {
				return false;
			}
			
			// 未接続の場合はそのまま終了
			if (_socket.connected == false) {
				return false;
			}
			
			// パケットのデータをソケットに書き込む
			try {
				packet.data.position = 0;
				_socket.writeShort(packet.type);
				_socket.writeShort(packet.data.length);
				_socket.writeBytes(packet.data, 0, packet.data.length);
				_socket.flush();
			}
			catch (e:IOError) {
				return false;
			}
			
			// デバッグ用メッセージ表示
			if (_debugMode) {
				debugTrace("sendPacket() : 送信イベント発行 : " + packet.toString());
			}
			
			// イベント発行
			var clientEvent:JoyClientEvent;
			clientEvent = new JoyClientEvent(JoyClientEvent.PACKET_SENT);
			clientEvent.text = packet.tag;
			dispatchEvent(clientEvent);
			
			return true;
		}
		
		/**
		 * データのエンディアン (並び順) を設定する.
		 */
		public function set endian(value:String):void 
		{
			_endian = value;
			if (_socket == null)
				return;
			_socket.endian = value;
		}
		
		/**
		 * データのエンディアン (並び順) を取得する.
		 */
		public function get endian():String
		{
			return _endian;
		}
		
		/*
		 * 接続完了までのタイムアウト時間 (ミリ秒) を設定する.
		 * Flash Player 10 以降でないと使用できないっぽいので今のところ停止
		 */
		/*public function set timeout(value:uint):void 
		{
			_timeout = value;
			if (_socket == null)
				return;
			//_socket.timeout = value;
		}*/
		
		/*
		 * 接続完了までのタイムアウト時間 (ミリ秒) を取得する.
		 */
		/*public function get timeout():uint
		{
			return _timeout;
		}*/
		
		/**
		 * デバッグモードを設定する.
		 * true にするとデバッグ画面に詳細なメッセージが出力される。
		 */
		public function set debug(value:Boolean):void 
		{
			_debugMode = value;
		}
		
		// --------------------------------------------------------------------
		// Private メソッド
		// --------------------------------------------------------------------
		
		/**
		 * デバッグメッセージを表示する.
		 * @param	text
		 */
		private function debugTrace(text:String):void 
		{
			if (_debugMode) {
				trace("JoyClient." + text);
			}
		}
		
		/**
		 * パケット受信時の処理.
		 */
		private function onReceivePakcet():void 
		{
			// パケットの後処理
			_packet.remain = 0;
			
			//
			_pingChecker.reset();
			
			// 受信イベント発行
			if (_packet.type < 100) {
				dispatchReceiveEvent();
			} else {
				switch (_packet.type) 
				{
					case DEVICE_LIST_PACKET_TYPE:
						dispatchDeviceListEvent();
						break;
				}
			}
		}
		
		/**
		 * データ受信イベントを発行する.
		 */
		private function dispatchReceiveEvent():void 
		{
			if (_debugMode) {
				debugTrace("dispatchReceiveEvent() : 受信イベント発行 : " + _packet.toString());
			}
			
			// ジョイスティックの状態のセット
			var state:JoystickState = new JoystickState();
			state.deviceNumber = _packet.type;
			state.x = _packet.data.readByte();
			state.y = _packet.data.readByte();
			for (var i:int = 0; i < 10; i++)
			{
				state.button[i] = _packet.data.readByte();
			}
			if (_prevState != null)
				state.update(_prevState);
			_prevState = state;
			
			// 受信イベント発行
			var clientEvent:JoyClientEvent;
			clientEvent = new JoyClientEvent(JoyClientEvent.PACKET_RECEIVED,
									      false,
									      false);
			clientEvent.state = state;
			dispatchEvent(clientEvent);
			_packet = null;
		}
		
		/**
		 * デバイスリストの応答イベントを発行する.
		 */
		private function dispatchDeviceListEvent():void 
		{
			if (_debugMode) {
				debugTrace("dispatchDeviceListEvent() : 受信イベント発行 : " + _packet.toString());
			}
			
			// デバイスリストの作成
			var deviceList:Array = new Array();
			var deviceCount:int = _packet.data.readUnsignedByte();
			
			for (var i:uint = 0; i < deviceCount; i++) {
				var name:String = _packet.data.readUTF();
				deviceList.push(name);
			}
			
			// 受信イベント発行
			var clientEvent:JoyClientEvent;
			clientEvent = new JoyClientEvent(JoyClientEvent.DEVICE_LIST,
									      false,
									      false);
			clientEvent.deviceList = deviceList;
			dispatchEvent(clientEvent);
			_packet = null;
		}
		
		// --------------------------------------------------------------------
		// イベントハンドラ
		// --------------------------------------------------------------------
		
		/**
		 * サーバに接続された時のイベント.
		 * @param	event
		 */
		private function onConnect(e:Event):void
		{
			debugTrace("onConnect() : ソケット接続完了");
			
			// Ping チェッカー開始
			_pingChecker.start();
			
			// イベント発行
			var clientEvent:JoyClientEvent;
			clientEvent = new JoyClientEvent(JoyClientEvent.CONNECTED);
			dispatchEvent(clientEvent);
		}
		
		/**
		 * ソケットが閉じられた時のイベント.
		 * @param	event
		 */
		private function onClose(e:Event):void
		{
			debugTrace("onClose() : ソケット切断完了");
			
			// Ping チェッカーを止める
			_pingChecker.stop();
			
			// イベント発行
			var clientEvent:JoyClientEvent;
			clientEvent = new JoyClientEvent(JoyClientEvent.CLOSED);
			dispatchEvent(clientEvent);
		}
		
		/**
		 * IO エラーイベント.
		 * @param	event
		 */
		private function onIoError(e:IOErrorEvent):void
		{
			debugTrace("onIoError() : IO エラー発生 : " + e.text);
			
			// Ping チェッカーを止める
			_pingChecker.stop();
			
			// イベント発行
			var clientEvent:JoyClientEvent;
			clientEvent = new JoyClientEvent(JoyClientEvent.IO_ERROR);
			clientEvent.text = e.text;
			dispatchEvent(clientEvent);
		}
		
		/**
		 * セキュリティエラーイベント.
		 * @param	event
		 */
		private function onSecurityError(e:SecurityErrorEvent):void
		{
			debugTrace("onSecurityError() : セキュリティエラー発生 : " + e.text);
			
			// Ping チェッカーを止める
			_pingChecker.stop();
			
			// イベント発行
			var clientEvent:JoyClientEvent;
			clientEvent = new JoyClientEvent(JoyClientEvent.SECURITY_ERROR);
			clientEvent.text = e.text;
			dispatchEvent(clientEvent);
		}

		/**
		 * データ受信イベント.
		 * @param	event
		 */
		private function onSocketData(e:ProgressEvent):void
		{
			// データが 1 バイトでも残っている時はループ
			while (_socket.bytesAvailable > 0)
			{
				// ソケットの残りデータが必要バイト数に満たない場合はループ終了
				if (_packetNecessaryBytesLength > _socket.bytesAvailable) {
					break;
				}
				
				try {
					// 現在の組み立てフェーズごとに処理分け
					switch (_packetPhase)
					{
						// パケットタイプ (Short)
						case 0:
							if (_socket.bytesAvailable >= 2) {
								_packet = new Packet();
								_packet.data.endian = _endian;
								_packet.type = _socket.readUnsignedShort();
								_packetNecessaryBytesLength = 0;
								_packetPhase++;
								
							} else {
								_packetNecessaryBytesLength = 2;
							}
							break;
						
						// パケット長 (Short)
						case 1:
							if (_socket.bytesAvailable >= 2) {
								_packet.remain = _socket.readUnsignedShort();
								_packetNecessaryBytesLength = 0;
								_packetPhase++;
								
								// データ部が無いパケットの場合
								if (_packet.remain == 0) {
									_packetPhase = 0;
									onReceivePakcet();
									break;
								}
							} else {
								_packetNecessaryBytesLength = 2;
							}
							break;
						
						// データ (ByteArray)
						case 2:
							// データの一部のみ受信できた場合
							if (_socket.bytesAvailable < _packet.remain) {
								_socket.readBytes(_packet.data, 
												  _packet.data.position, 
												  _socket.bytesAvailable);
								_packet.remain -= _socket.bytesAvailable;
							}
							// 残り全てが受信できた場合
							else {
								_socket.readBytes(_packet.data, 
												  _packet.data.position, 
												  _packet.remain);
								_packetNecessaryBytesLength = 0;
								_packetPhase = 0;
								onReceivePakcet();
							}
							break;
					}
				}
				// ソケットで I/O エラーが発生したか、ソケットが開いていない時に発生
				catch (e:IOError) {
					return;
				}
			}
		}
	
		/**
		 * Ping カウントチェッカー.
		 * タイムアウトに達した時
		 * @param	e
		 */
		private function onTimer(e:TimerEvent):void 
		{
			close();
		}
		
		
	}
	
}